package main

import (
	"fmt"
	"net/http"
	"os"
	"strings"

	"gitee.com/zacyuan/yuan/pkg/share"
	"github.com/dave/dst"
	"github.com/dave/dst/decorator"
	"github.com/spf13/cobra"
)

const (
	tplStr = `// 自动生成的文件，请不要修改。
package %s

import (
	"github.com/gin-gonic/gin"
)

type GinHandleFunc func(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes

func HandleMethod(method func(*gin.Context) (any, error)) func(*gin.Context) {
	return func(ctx *gin.Context) {
		data, err := method(ctx)
		if err != nil {
			ctx.JSON(200, gin.H{"ret": -999, "msg": err.Error()})
			return
		}

		if data == nil {
			ctx.JSON(200, gin.H{"ret": 0, "msg": "ok"})
			return
		}

		ctx.JSON(200, gin.H{"ret": 0, "msg": "ok", "data": data})
	}
}

func RegisterControl(handel GinHandleFunc) {
%s
%s
}
`

	httpMethodSplit = "@HttpMethod"
)

var (
	allHttpMethod = []string{
		http.MethodGet,
		http.MethodHead,
		http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete,
		http.MethodConnect,
		http.MethodOptions,
		http.MethodTrace,
	}
)

type Ctrl struct {
	CtrlName   string
	HttpMethod []string
	Handle     string
}

func init() {
	var cmd = &cobra.Command{
		Use:   "ctrl <name>",
		Args:  cobra.MinimumNArgs(0),
		Short: "自动生成gin路由注册方法。",
		Long: `自动生成gin路由注册方法。
For example:
  ctrl -dir=/root/zacyuan/controller`,
		Run: register_controller,
	}

	cmd.Flags().StringP("dir", "d", "", "controller所在目录。")
	cmd.Flags().StringP("output", "o", "", "输出文件名。")
	rootCmd.AddCommand(cmd)
}

func register_controller(cmd *cobra.Command, args []string) {
	dir := cmd.Flag("dir").Value.String()
	output := cmd.Flag("output").Value.String()

	if dir == "" {
		dir = "./"
	}

	if dir[len(dir)-1] != '/' {
		dir += "/"
	}

	if output == "" {
		output = "register_gen.go"
	}

	codeGen(dir, output)
}

func codeGen(dir, output string) {
	files, err := os.ReadDir(dir)
	if err != nil {
		panic(err)
	}

	var ctrls []*Ctrl
	pkgName := ""
	for _, file := range files {
		if file.IsDir() {
			continue
		}

		if file.Name() == output {
			continue
		}

		fileName := dir + file.Name()
		buf, err := os.ReadFile(fileName)
		if err != nil {
			continue
		}

		code := string(buf)
		f, err := decorator.Parse(code)
		if err != nil {
			panic(err)
		}

		pkgName = f.Name.Name

		dst.Inspect(f, func(node dst.Node) bool {
			fDecl, ok := node.(*dst.FuncDecl)
			if !ok {
				return true
			}

			if fDecl.Recv == nil || fDecl.Type == nil ||
				fDecl.Type.Params == nil || fDecl.Type.Params.List == nil || len(fDecl.Type.Params.List) != 1 ||
				fDecl.Type.Results == nil || fDecl.Type.Results.List == nil || len(fDecl.Type.Results.List) != 2 {
				return true
			}

			if !isRightParam(fDecl.Type.Params.List[0]) {
				return true
			}

			if !isRightReturn(fDecl.Type.Results.List[0], fDecl.Type.Results.List[1]) {
				return true
			}

			first := fDecl.Name.Name[0:1]
			if strings.ToUpper(first) != first {
				return true
			}

			ctrl := &Ctrl{
				CtrlName:   structName(fDecl.Recv),
				HttpMethod: httpMethod(fDecl),
				Handle:     fDecl.Name.Name,
			}
			ctrls = append(ctrls, ctrl)

			return true
		})

	}

	mCtrls := make(map[string]string)
	var sbCtrl strings.Builder
	var sbMethod strings.Builder
	for _, ctrl := range ctrls {
		ctrlName := strings.ToLower(ctrl.CtrlName[0:1]) + ctrl.CtrlName[1:]
		if _, ok := mCtrls[ctrl.CtrlName]; !ok {
			sbCtrl.WriteString("    ")
			sbCtrl.WriteString(ctrlName)
			sbCtrl.WriteString(" := &")
			sbCtrl.WriteString(ctrl.CtrlName)
			sbCtrl.WriteString("{}\n")
			mCtrls[ctrl.CtrlName] = ctrl.CtrlName
		}

		for _, one := range ctrl.HttpMethod {
			sbMethod.WriteString("    handel(")
			sbMethod.WriteString("\"")
			sbMethod.WriteString(one)
			sbMethod.WriteString("\"")
			sbMethod.WriteString(", \"/")
			sbMethod.WriteString(share.SnakeString(ctrl.CtrlName))
			sbMethod.WriteString("/")
			sbMethod.WriteString(share.SnakeString(ctrl.Handle))
			sbMethod.WriteString("\", HandleMethod(")
			sbMethod.WriteString(ctrlName)
			sbMethod.WriteString(".")
			sbMethod.WriteString(ctrl.Handle)
			sbMethod.WriteString("))\n")
		}
	}

	fmt.Println(pkgName)
	fmt.Println(sbCtrl.String())
	fmt.Println(sbMethod.String())

	str := fmt.Sprintf(tplStr, pkgName, sbCtrl.String(), sbMethod.String())

	err = os.WriteFile(dir+output, []byte(str), os.FileMode(0644))
	if err != nil {
		fmt.Println(err)
		return
	}
}

func isRightParam(field *dst.Field) bool {
	params, ok := field.Type.(*dst.StarExpr)
	if !ok {
		return false
	}

	if params.X == nil {
		return false
	}

	x, ok := params.X.(*dst.SelectorExpr)
	if !ok {
		return false
	}

	if x.X == nil || x.Sel == nil {
		return false
	}

	if x.Sel.Name != "Context" {
		return false
	}

	ident, ok := x.X.(*dst.Ident)
	if !ok {
		return false
	}
	if ident.Name != "gin" {
		return false
	}

	return true
}

func isRightReturn(field1, field2 *dst.Field) bool {
	params1, ok := field1.Type.(*dst.Ident)
	if !ok {
		return false
	}
	if params1.Name != "any" {
		return false
	}

	params2, ok := field2.Type.(*dst.Ident)
	if !ok {
		return false
	}

	if params2.Name != "error" {
		return false
	}

	return true
}

func structName(recv *dst.FieldList) string {
	if len(recv.List) == 0 || recv.List[0].Type == nil {
		return ""
	}

	field, ok := recv.List[0].Type.(*dst.Ident)
	if !ok {
		return ""
	}

	return field.Name
}

func httpMethod(fDecl *dst.FuncDecl) []string {
	def := []string{http.MethodGet, http.MethodPost}

	var methods []string
	for _, one := range fDecl.Decs.NodeDecs.Start {
		if !strings.Contains(one, httpMethodSplit) {
			continue
		}

		tmp := strings.Split(one, httpMethodSplit)
		if len(tmp) != 2 {
			continue
		}
		tmp = strings.Split(strings.TrimSpace(tmp[1]), " ")
		if len(tmp) == 0 {
			continue
		}

		for _, m := range tmp {
			m = strings.ToUpper(strings.TrimSpace(m))
			if m == "" {
				continue
			}

			if !isRightHttpMethod(m) {
				continue
			}

			methods = append(methods, m)
		}
	}

	if len(methods) > 0 {
		return methods
	}

	return def
}

func isRightHttpMethod(str string) bool {
	for _, one := range allHttpMethod {
		if one == str {
			return true
		}
	}
	return false
}
